//
// Created by entaoyang@163.com on 2017/10/10.
// Copyright (c) 2017 yet.net. All rights reserved.
//

import Foundation

public extension NSObjectProtocol {

    @discardableResult
    func apply(_ block: (Self) -> Void) -> Self {
        block(self)
        return self
    }
}

private var attr_key = "_attrkey_"

private class MapWrap {
    var map = [String: Any]()
}

public extension NSObject {


    func setRetainObject(_ key: UnsafeRawPointer, _ value: Any?) {
        objc_setAssociatedObject(self, key, value, .OBJC_ASSOCIATION_RETAIN)
    }

    func getRetainObject(_ key: UnsafeRawPointer) -> Any? {
        objc_getAssociatedObject(self, key)
    }


    private var _attrMap: MapWrap {
        if let m = objc_getAssociatedObject(self, &attr_key) as? MapWrap {
            return m
        } else {
            let map = MapWrap()
            objc_setAssociatedObject(self, &attr_key, map, .OBJC_ASSOCIATION_RETAIN)
            return map
        }
    }

    func setAttr(_ key: String, _ value: Any?) {
        if let v = value {
            self._attrMap.map[key] = v
        } else {
            self._attrMap.map.removeValue(forKey: key)
        }
    }

    func getAttr(_ key: String) -> Any? {
        _attrMap.map[key]
    }

    func getOrPut(key: String, _ block: () -> Any) -> Any {
        if let v = getAttr(key) {
            return v
        }
        let v = block()
        setAttr(key, v)
        return v
    }

    func ensureAttr<T: Any>(key: String, _ block: () -> T) -> T {
        if key.isEmpty {
            fatalError("ensureAttr key is empty!")
        }
        if let v = getAttr(key) {
            return v as! T
        }
        let v = block()
        setAttr(key, v)
        return v
    }

    var tagS: String {
        get {
            (getAttr("_tagS_") as? String) ?? ""
        }
        set {
            setAttr("_tagS_", newValue)
        }
    }
}


public typealias NotificationCallback = (Notification) -> Void

public extension NSObject {


    fileprivate var notifyCallback: NotificationCallback? {
        get {
            getAttr("_notification_callback_") as? NotificationCallback
        }
        set {
            setAttr("_notification_callback_", newValue)
        }
    }

    @objc
    fileprivate func onNotificationRecv(_ n: Notification) {
        self.notifyCallback?(n)
    }

    func watch(object: NSObject, name: Notification.Name, _ block: @escaping NotificationCallback) {
        self.notifyCallback = block
        NotificationCenter.default.addObserver(self, selector: #selector(onNotificationRecv(_:)), name: name, object: object)
    }


    func watchRemove(object: NSObject, name: Notification.Name) {
        NotificationCenter.default.removeObserver(self, name: name, object: object)
    }


    func postNotify(name: Notification.Name) {
        NotificationCenter.default.post(name: name, object: self)
    }

    func postNotify(name: Notification.Name, userInfo: [AnyHashable: Any]) {
        NotificationCenter.default.post(name: name, object: self, userInfo: userInfo)
    }
}


public class PropChangedInfo: Applyable {
    public unowned let obj: NSObject
    public let keyPath: String
    public var oldValue: Any? = nil
    public var newValue: Any? = nil

    public init(_ obj: NSObject, _ keyPath: String) {
        self.obj = obj
        self.keyPath = keyPath
    }
}

public typealias PropChangedCallback = (PropChangedInfo) -> Void

public extension NSObject {

    fileprivate var propChangedCallback: PropChangedCallback? {
        get {
            getAttr("_prop_changed_callback_") as? PropChangedCallback
        }
        set {
            setAttr("_prop_changed_callback_", newValue)
        }
    }

    func onProp(changed keyPath: String, _ callback: @escaping PropChangedCallback) {
        self.propChangedCallback = callback
        addObserver(PropObserver.inst, forKeyPath: keyPath, options: [.old, .new], context: nil)
    }

    func onProps(changed keyPaths: [String], _ callback: @escaping PropChangedCallback) {
        self.propChangedCallback = callback
        for s in keyPaths {
            addObserver(PropObserver.inst, forKeyPath: s, options: [.old, .new], context: nil)
        }
    }
}

fileprivate class PropObserver: NSObject {
    private override init() {

    }

    fileprivate static let inst = PropObserver()

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        if let obj = object as? NSObject, let kp = keyPath {
            let info = PropChangedInfo(obj, kp)
            info.newValue = change?[.newKey]
            info.oldValue = change?[.oldKey]
            obj.propChangedCallback?(info)
        }
    }

}
